#!/usr/bin/env python

# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Street Identity Console."""

import cgi
import logging
import os
import pickle
import random
import account
from base import BaseHandler
import certs
import common
from common import SESSION_ID
from common import SUCCESSFUL_LOGIN_REDIRECT
import developer
from google.appengine.api import memcache
from google.appengine.ext import db
import image
import jinja2
from lib import streetidentity
from lib import streetidentity_manage
from lib import authn
from lib import http
from lib.loggable import loggable
import model
from openidconnect.client import openidconnect_flow_from_clientsecrets
from openidconnect.error import FlowUserInfoError
import webapp2

__author__ = 'Maciej Machulak'
__maintainer__ = 'Maciej Machulak'
__email__ = 'mmachulak@google.com'

__copyright__ = 'Copyright 2012, Google Inc.'
__license__ = 'Apache License 2.0'
__version__ = '0.2'
__status__ = 'Prototype'

IDP_FLOW = openidconnect_flow_from_clientsecrets('config/client_secrets.json',
  scope='https://www.googleapis.com/auth/userinfo.profile '\
        'https://www.googleapis.com/auth/userinfo.email'
)

jinja_environment = jinja2.Environment(
  loader=jinja2.FileSystemLoader(
    os.path.join(os.path.dirname(__file__),'templates/')))

log = logging.getLogger()
log.setLevel(logging.DEBUG)


class MainHandler(BaseHandler):
  """Main handler for the application."""

  @authn.redirect_logged_user(SUCCESSFUL_LOGIN_REDIRECT)
  @loggable('main page')
  def get(self):
    """Displays the main page of the application."""

    template = jinja_environment.get_template('index.html')
    self.response.out.write(template.render({}))


class SignInHandler(BaseHandler):
  """Redirects the user to Google's OAuth 2 Authorization Endpoint."""

  @loggable('sign in')
  def get(self):
    """Redirects the user to Google's OAuth 2 Authorization Endpoint."""

    callback = self.request.relative_url(common.CALLBACK_URL)
    log.debug('callback URL: %s', callback)
    authorize_url = IDP_FLOW.step1_get_authorize_url(callback)
    log.debug('redirecting user to %s', authorize_url)
    self.redirect(str(authorize_url))


class SignOutHandler(BaseHandler):
  """Destroys the session for the user."""

  @authn.login_required()
  @loggable('sign out')
  def get(self):
    """Signs out the user from the application by destroying a session."""

    # Delete session from the database
    logging.debug('retrieving session from persistent store')
    session = model.Session.all().filter('id = ', self.session.id).get()
    if session:
      log.debug('session found - deleting.')
      session.delete()

    # Delete session from memcache
    status = memcache.delete(self.session.id)
    log.debug('status of removing session from memcache %s', status)

    # Delete session information from the header
    self.response.headers.add_header(http.Header.set_cookie,
      SESSION_ID + '=' + '; Path=/')
    log.debug('removed session information from the header')

    # Redirect the user back to mainpage
    self.redirect('/')


class OAuthHandler(BaseHandler):
  """OAuth Redirect handler."""

  def get(self):
    """Do the OpenID Connect Flow."""

    logging.debug('openid connect flow')
    try:
      openidconnect_credentials = \
      IDP_FLOW.step234_exchange_and_tokeninfo_and_userinfo(self.request.params)

      logging.debug(
        'obtained user info %s: ', openidconnect_credentials.userinfo)

      user_info = openidconnect_credentials.userinfo

      # Required user attribute - id of the user
      user_id = user_info.id
      logging.debug('user id: %s', user_id)

      # Retrieve existing user
      user = model.User.all().filter('user_id = ', user_id).get()

      if user is None or user.refresh_token is None\
      or user.email is None:
        # If user does not exist yet, does not have a refresh token, or does
        # not have an email registered then redirect to 'Terms of Service' page
        logging.debug(
          'user not found - creating temporary session and '
          'redirecting to ToS page')

        # Generate session id
        key = str(random.random())[2:] + str(random.random())[2:]

        # Create session for the user
        session = model.Session(id=key, credentials=db.Blob(
          pickle.dumps(openidconnect_credentials)))
        session.put()
        logging.debug('session stored within the datastore')

        # Store the session within memcache
        memcache.add(session.id, session)
        logging.debug('session stored within memcache')

        self.response.headers.add_header(http.Header.set_cookie,
          SESSION_ID + '=' + key + '; Path=/')
        logging.debug('session set: ' + str(session.id))

        # redirect to registration page
        self.redirect('/register')
        return

      else:
        logging.debug('user found')

      # Access token for the user
      access_token = openidconnect_credentials.access_token

      # Generate session id
      key = str(random.random())[2:] + str(random.random())[2:]

      # Create session for the user
      session = model.Session(id=key, user=user,
        access_token=access_token)
      session.put()
      logging.debug('session stored within the datastore')

      # Store the session within memcache
      memcache.add(session.id, session)
      logging.debug('session stored within memcache')

      self.response.headers.add_header(http.Header.set_cookie,
        SESSION_ID + '=' + key + '; Path=/')
      logging.debug('session set: ' + str(session.id))
      self.redirect(SUCCESSFUL_LOGIN_REDIRECT)

    except FlowUserInfoError:
      logging.error(
        'error during OAuth dance - could not retrieve user info')
      values = {
        'error_message': 'user info could not be retrieved'
      }
      template = jinja_environment.get_template('templates/error.html')
      self.response.out.write(template.render(values))
      return


class RegistrationHandler(BaseHandler):
  """Handles requests during user registration process."""

  @authn.login_required(registered=False)
  @loggable('show registration page')
  def get(self):
    """Displays the registration page."""

    # Load credentials from the database
    credentials = pickle.loads(str(self.session.credentials))
    logging.debug('openid connect credentials: %s' + str(credentials))

    values = {
      'display_name': credentials.userinfo.name,
      'email': credentials.userinfo.email
    }
    template = jinja_environment.get_template('tos.html')
    self.response.out.write(template.render(values))

  @authn.login_required(registered=False)
  @loggable('register user')
  def post(self):
    """Processes user-supplied data during user registration process."""
    accept = cgi.escape(self.request.get('accept'))
    if accept == 'true':
      logging.debug(
        'user accepted terms of service - creating a local user')

      # Get saved credentials from the session
      openidconnect_credentials = pickle.loads(
        str(self.session.credentials))
      user_id = openidconnect_credentials.userinfo.id
      display_name = openidconnect_credentials.userinfo.name
      email = openidconnect_credentials.userinfo.email
      refresh_token = openidconnect_credentials.refresh_token

      # Check if the user is present
      user = model.User.all().filter('user_id = ', user_id).get()
      if user is None:
        # Create a new user
        logging.debug('user not found - creating new one')
        user = model.User(user_id=user_id)

      # Update the user
      user.name = display_name
      user.email = email
      user.display_name = display_name
      user.refresh_token = refresh_token

      user.put()
      logging.debug('user updated successfully')

      # Associate the user with the session
      session = self.session
      session.user = user
      session.access_token = openidconnect_credentials.access_token

      # Clear credentials from the session
      session.credentials = None

      # Store the session within the database
      session.put()
      logging.debug('session stored within the datastore')

      # Store the session within memcache
      memcache.replace(session.id, session)
      logging.debug('session replaced within memcache')

      self.redirect('/account')
      return
    else:
      logging.debug(
        'user rejected terms of service - deleting session cookie')
      self.response.headers.add_header(http.Header.set_cookie,
        SESSION_ID + '=')
      logging.debug('redirecting user to main page')
      self.redirect('/')
      return


routes = [('/', MainHandler),

  ('/sign_in', SignInHandler),
  ('/sign_out', SignOutHandler),
  ('/register', RegistrationHandler),

  (common.CALLBACK_URL, OAuthHandler),

  ('/account', account.AccountHandler),
  ('/developer', developer.DeveloperHandler),
  ('/developer/remove', developer.DeveloperRemoveAppHandler),

  ('/icon', image.ImageHandler),

  # --- STREET IDENTITY HANDLERS - START ---

  # Handler used by the RP to discover registered attribute providers
  ('/streetidentity/v1beta/discovery',
   streetidentity.DiscoveryHandler),

  # Handler used by the RP to obtain a token for an attribute
  ('/streetidentity/v1beta/token',
   streetidentity.TokenHandler),

  # Handler used by the AP to verify the token received from the RP
  ('/streetidentity/v1beta/tokeninfo',
   streetidentity.TokenInfoHandler),

  # Exposes public certs for APs for static validation of RP tokens
  ('/certs', certs.CertsHandler),

  # --- STREET IDENTITY HANDLERS - STOP ---

  # --- STREET IDENTITY TEMPORARY HANDLERS - START ---

  # Handler for AP to register itself
  ('/streetidentity_manage/v1beta/register',
   streetidentity_manage.RegistrationHandler),

  # Handler for AP to deregister itself
  ('/streetidentity_manage/v1beta/deregister',
   streetidentity_manage.DeRegistrationHandler), ]

  # --- STREET IDENTITY TEMPORARY HANDLERS - STOP ---

application = webapp2.WSGIApplication(routes, debug=True)
